Skip to content

fix: middleware crashes when run is created without a context dict - #1

Merged
virtaava merged 2 commits into
mainfrom
fix-middleware-context-fallback
Apr 25, 2026
Merged

fix: middleware crashes when run is created without a context dict#1
virtaava merged 2 commits into
mainfrom
fix-middleware-context-fallback

Conversation

@virtaava

Copy link
Copy Markdown
Owner

Summary

External LangGraph callers (SDK, Studio UI, plain REST) typically don't pass a context dict on run creation, so runtime.context = None. DeerFlow's middlewares unconditionally called runtime.context.get(...) and crashed with AttributeError: 'NoneType' object has no attribute 'get' on the very first hop (ThreadDataMiddleware.before_agent). Every external run failed in 600 ms.

Fix

New helper module backend/src/agents/middlewares/_runtime_helpers.py:

  • resolve_runtime_value(runtime, key) -> Any | None — checks runtime.context first, then falls back to config.configurable (where LangGraph Server populates thread_id from the URL path). Never raises.
  • require_thread_id(runtime) -> str — same lookup, but raises ValueError with a clear message if the thread_id genuinely can't be found anywhere.

Applied at every middleware call site that previously did the raw pattern:

File Change
agents/middlewares/thread_data_middleware.py uses require_thread_id()
agents/middlewares/uploads_middleware.py uses resolve_runtime_value()
agents/middlewares/memory_middleware.py uses resolve_runtime_value()
sandbox/middleware.py uses both (before_agent + after_agent)

Verified end-to-end

A lead_agent run created with only {assistant_id, input} (no context) now completes successfully — "Reply with only the word OK." → "OK". Same payload shape was crashing in 600 ms before the fix.

Out of scope

  • BudgetEnforcementMiddleware._get_thread_id() and LoopDetectionMiddleware._get_thread_id() already had isinstance(runtime.context, dict) guards — left alone.
  • sandbox/tools.py and tools/builtins/* still use the raw pattern. They only run when the middleware chain has populated thread_id, so they're not the bug, but could be migrated to the helper for consistency in a follow-up.

🤖 Generated with Claude Code

virtaava and others added 2 commits March 18, 2026 12:22
7 harness tools replacing free-form sandbox tools for constrained tasks:
- harness_read_file: tracks files_read, 200-line window with line numbers
- harness_search: capped at 30 results with "refine" feedback
- harness_propose_patch: validates via linters + git apply --check inline
- harness_apply_patch: only if proposal validated
- harness_run_tests: structured results, gates commit
- harness_commit: blocked unless tests pass
- harness_status: shows current state and next valid actions

Custom 'harness' agent config with tool_groups: [harness, web].
SOUL.md enforces read→propose→apply→test→commit workflow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LangGraph Server runs created without an explicit `context` dict (the
default for the LangGraph SDK, Studio UI, and most external REST callers)
landed in DeerFlow with `runtime.context = None`. Several middlewares
unconditionally called `runtime.context.get(...)`, which raised
`AttributeError: 'NoneType' object has no attribute 'get'` on the very
first hop (`ThreadDataMiddleware.before_agent`) — every external run
failed in 600ms.

This change adds `src/agents/middlewares/_runtime_helpers.py` with two
small helpers:

  resolve_runtime_value(runtime, key)  ->  Any | None
      Look up key in runtime.context first; if absent, fall back to
      LangGraph-injected config.configurable (where Server puts thread_id
      from the URL path). Never raises.

  require_thread_id(runtime)  ->  str
      Same lookup but raises ValueError with a meaningful message instead
      of AttributeError if the thread_id can't be found anywhere.

The helpers are applied at every call site that previously did the raw
`runtime.context.get("thread_id")` / `runtime.context["thread_id"]` /
`runtime.context.get("sandbox_id")` pattern in middlewares:

  backend/src/agents/middlewares/thread_data_middleware.py
  backend/src/agents/middlewares/uploads_middleware.py
  backend/src/agents/middlewares/memory_middleware.py
  backend/src/sandbox/middleware.py

Verified end-to-end: a `lead_agent` run created with only
`{assistant_id, input}` (no context) now completes successfully
("Reply with only the word OK." -> "OK"). Same payload shape failed
in 600ms before the fix.

Other defensive call sites (BudgetEnforcementMiddleware._get_thread_id,
LoopDetectionMiddleware._get_thread_id) already had isinstance guards
and are not touched. Sandbox/tools.py and src/tools/builtins/* still use
the raw pattern; left alone here because they only run when the
middleware chain has already populated thread_id, but they could be
migrated to the helper in a follow-up for consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@virtaava
virtaava merged commit 373e89d into main Apr 25, 2026
1 of 3 checks passed
@virtaava
virtaava deleted the fix-middleware-context-fallback branch April 25, 2026 18:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant